﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics.Contracts;
using System.Reflection;
using System.ComponentModel;
using System.Collections;
using System.Threading;
using System.Linq.Expressions;
using System.Windows;

namespace ReactiveCode.Observables
{
    using Util;

    public static partial class Extensions
    {

#if CONTRACT_DEBUGGING

        static Extensions()
        {
            ContractDebugging.Initialize();
        }

#endif

        public static IObservable<T> IIf<T>(this IObservable<bool> test, IObservable<T> trueValue, IObservable<T> falseValue)
        {
            Contract.Requires(test != null && trueValue != null && falseValue != null);
            return test.SelectMany(x => x ? trueValue : falseValue);
        }

        internal static LatestValue<T> LatestContinuous<T>(this IObservable<T> source, bool distinct = true)
        {
            Contract.Requires(source != null);
            return new LatestValue<T>(source, distinct);
        }

        internal static IObservable<T[]> CombineLatest<T>(this IEnumerable<IObservable<T>> observables)
        {
            Contract.Requires(observables != null);

            var array = observables.ToArray();
            if (array.Contains(null))
                throw new ArgumentNullException();
            if (array.Length == 0)
                return Observable.Return(new T[0]);

            return Observable.Create<T[]>(o =>
            {
                T[] latestValues = new T[array.Length];
                bool hasAllValues = false;
                bool[] hasValues = new bool[array.Length];
                var subscriptions = new IDisposable[array.Length];

                for (int _i = 0; _i < array.Length; _i++)
                {
                    int i = _i;
                    subscriptions[i] = array[i].Subscribe(
                        next =>
                        {
                            hasValues[i] = true;
                            latestValues[i] = next;
                            if (!hasAllValues && hasValues.All(v => v))
                                hasAllValues = true;

                            if (hasAllValues)
                                o.OnNext(latestValues.ToArray());
                        },
                        o.OnError,
                        o.OnCompleted);
                }

                return delegate
                {
                    foreach (var s in subscriptions)
                        s.Dispose();
                };
            });
        }

        #region FromEvent

        class EventObserverHandle<TEventArgs> : IDisposable
        {
            class Event : IEvent<TEventArgs>
            {
                public TEventArgs EventArgs { get; private set; }
                public object Sender { get; private set; }

                public Event(object sender, TEventArgs eventArgs)
                {
                    Sender = sender;
                    EventArgs = eventArgs;
                }
            }

            IObserver<IEvent<TEventArgs>> _observer;
            Action _removeHandler;

            public EventObserverHandle(object target, EventInfo @event, IObserver<IEvent<TEventArgs>> observer)
            {
                Contract.Requires(@event != null);
                Contract.Requires(observer != null);
                _observer = observer;
                var handler = Delegate.CreateDelegate(@event.EventHandlerType, this, "Handle");
                @event.AddEventHandler(target, handler);
                _removeHandler = () => @event.RemoveEventHandler(target, handler);
            }
            public void Dispose()
            {
                _removeHandler();
            }

            public void Handle(object sender, TEventArgs e)
            {
                _observer.OnNext(new Event(sender, e));
            }
        }

        internal static IObservable<IEvent<TEventArgs>> FromEvent<TEventArgs>(object target, EventInfo @event)
        {
            Contract.Requires(@event != null && @event.DeclaringType != null);
            Contract.Requires(@event.GetAddMethod() != null && @event.GetRemoveMethod() != null);
            Contract.Requires((target == null) == @event.GetAddMethod().IsStatic);
            Contract.Requires(target == null || @event.DeclaringType.IsAssignableFrom(target.GetType()));

            return Observable.CreateWithDisposable<IEvent<TEventArgs>>(o => new EventObserverHandle<TEventArgs>(target, @event, o));
        }

        #endregion

        #region Property/field observing

        static EventInfo _propertyChangedEvent = typeof(INotifyPropertyChanged).GetEvent("PropertyChanged");
        internal static IObservable<PropertyChangedEventArgs> ObservePropertyChanged(this INotifyPropertyChanged obj, string propertyName)
        {
            Contract.Requires(obj != null);

            var result = from @event in FromEvent<PropertyChangedEventArgs>(obj, _propertyChangedEvent)
                         select @event.EventArgs;

            if (!string.IsNullOrEmpty(propertyName))
            {
                result = from e in result
                         where e == null || string.IsNullOrEmpty(e.PropertyName) || e.PropertyName == propertyName
                         select e;
            }

            return result;
        }

        internal static IObservable<object> ObserveDependencyProperty(DependencyObject instance, DependencyProperty property)
        {
            return Observable.CreateWithDisposable<object>(o => new DependencyPropertyObserverHandle(instance, property, o));
        }

        #endregion

        public static IDisposable Bind<T>(this ISubject<T> lhs, ISubject<T> rhs)
        {
            Contract.Requires(lhs != null && rhs != null);
            Contract.Requires(lhs != rhs);

            bool syncing = false;

            Func<ISubject<T>, ISubject<T>, IDisposable> bind =
                (a, b) => a.Subscribe(
                    onNext: x =>
                    {
                        if (syncing)
                            return;
                        syncing = true;
                        try
                        {
                            b.OnNext(x);
                        }
                        finally
                        {
                            syncing = false;
                        }
                    },
                    onError: rhs.OnError, onCompleted: rhs.OnCompleted);
            var s1 = bind(lhs, rhs);
            var s2 = bind(rhs, lhs);
            return Disposable.Create(delegate
            {
                s1.Dispose();
                s2.Dispose();
            });
        }

        internal static IObservable<T> SubscribeIf<T>(this IObservable<T> source, IObservable<bool> test)
        {
            Contract.Requires(source != null && test != null);
            return Observable.Create<T>(o =>
            {
                IDisposable srcSubscription = null;
                var testSubscription = test.Subscribe(
                    x =>
                    {
                        if (x)
                        {
                            if (srcSubscription == null)
                                srcSubscription = source.Subscribe(o.OnNext, o.OnError);
                        }
                        else
                        {
                            if (srcSubscription != null)
                            {
                                srcSubscription.Dispose();
                                srcSubscription = null;
                            }
                        }
                    
                    }, o.OnError);
                return delegate
                {
                    testSubscription.Dispose();
                    if (srcSubscription != null)
                        srcSubscription.Dispose();
                };

            });
        }

        public static IObserver<T> Decorate<T>(this IObserver<T> observer, IStateDecorator stateDecorator)
        {
            Contract.Requires(observer != null && stateDecorator != null);
            return stateDecorator.Apply(observer);
        }
        public static IObservable<T> Decorate<T>(this IObservable<T> observable, IStateDecorator stateDecorator)
        {
            Contract.Requires(observable != null && stateDecorator != null);
            return stateDecorator.Apply(observable);
        }
        public static ISubject<T> Decorate<T>(this ISubject<T> subject, IStateDecorator stateDecorator)
        {
            Contract.Requires(subject != null && stateDecorator != null);
            IObservable<T> observable = subject;
            observable = stateDecorator.Apply(observable);
            IObserver<T> observer = subject;
            observer = stateDecorator.Apply(observer);
            return Subject.Create(observable, observer);
        }

        internal static IObservable<T> Clone<T>(this IObservable<T> source)
        {
            Contract.Requires(source != null);
            return Observable.CreateWithDisposable<T>(source.Subscribe);
        }
        public static IObservable<Unit> AsUnit<T>(this IObservable<T> source)
        {
            Contract.Requires(source != null);
            return source.Select(_ => new Unit());
        }
        public static IObservable<Unit> CombineLatest(this IObservable<Unit> first, IObservable<Unit> second)
        {
            Contract.Requires(first != null && second != null);
            return first.CombineLatest(second, (x, y) => x);
        }
        public static IObservable<Unit> WithInitial(this IObservable<Unit> source)
        {
            Contract.Requires(source != null);
            return Observable.CreateWithDisposable<Unit>(o =>
            {
                o.OnNext(new Unit());
                return source.Subscribe(o);
            });
        }


        internal static IObservable<T> MakeWeak<T>(this IObservable<T> source, out IDisposable handle)
        {
            var list = new LinkedList<object>();
            handle = Disposable.Create(delegate
            {
                list.Clear();
                list = null;
            });
            var result = source.MakeWeak(new WeakReference<ICollection<object>>(list));
            list.AddFirst(result);
            return result;
        }
        static IObservable<T> MakeWeak<T>(this IObservable<T> source, WeakReference<ICollection<object>> wrHandle)
        {
            Func<WeakReference<IObserver<T>>, IDisposable> subscribe = wrObserver =>
            {
                IDisposable subscription = null;
                Action<Action<IObserver<T>>> ifAlive = action =>
                {
                    if (wrObserver.IsAlive) 
                        action(wrObserver.Target);
                    else 
                        subscription.Dispose();
                };
                subscription = source.Subscribe(
                    onNext: x => ifAlive(o => o.OnNext(x)),
                    onError: err => ifAlive(o => o.OnError(err)),
                    onCompleted: () => ifAlive(o => o.OnCompleted())
                );
                subscription = subscription.Concat(delegate
                {
                    if (wrHandle.IsAlive && wrObserver.IsAlive)
                        wrHandle.Target.Remove(wrObserver.Target);
                });
                return subscription;
            };

            return Observable.CreateWithDisposable<T>(o =>
            {
                if (!wrHandle.IsAlive)
                {
                    Contract.Assert(!wrHandle.IsAlive);
                    return Disposable.Empty;
                }
                wrHandle.Target.Add(o);
                return subscribe(new WeakReference<IObserver<T>>(o));
            });
        }
    }
}
